
package com.huaweicloud.saaslanding.install.applicationservice.impl;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import com.huaweicloud.saaslanding.basic.database.mybatis.conditions.query.QueryWrap;
import com.huaweicloud.saaslanding.basic.enumeration.TenantAppDomainStatusEnum;
import com.huaweicloud.saaslanding.basic.exception.BizException;
import com.huaweicloud.saaslanding.basic.exception.code.ExceptionCode;
import com.huaweicloud.saaslanding.basic.utils.CheckNullUtil;
import com.huaweicloud.saaslanding.basic.utils.ThreadPoolManager;
import com.huaweicloud.saaslanding.dao.common.entity.app.AppTemplate;
import com.huaweicloud.saaslanding.dao.common.entity.deployedresource.CdnResource;
import com.huaweicloud.saaslanding.dao.common.entity.deployedresource.ObsResource;
import com.huaweicloud.saaslanding.dao.common.entity.deployedresource.WafHostResource;
import com.huaweicloud.saaslanding.dao.common.entity.tenant.TenantAppDomain;
import com.huaweicloud.saaslanding.dao.common.repository.app.AppTemplateBaseMapper;
import com.huaweicloud.saaslanding.dao.common.repository.deployedresource.CdnResourceBaseMapper;
import com.huaweicloud.saaslanding.dao.common.repository.deployedresource.ObsResourceBaseMapper;
import com.huaweicloud.saaslanding.dao.common.repository.deployedresource.WafHostResourceBaseMapper;
import com.huaweicloud.saaslanding.dao.common.repository.tenant.TenantAppDomainBaseMapper;
import com.huaweicloud.saaslanding.huawei.cdn.api.CdnCacheApi;
import com.huaweicloud.saaslanding.huawei.cdn.api.CdnDomainApi;
import com.huaweicloud.saaslanding.huawei.cdn.api.CdnFlexibleOriginApi;
import com.huaweicloud.saaslanding.huawei.cdn.api.CdnHttpsConfigApi;
import com.huaweicloud.saaslanding.huawei.cdn.enumeration.ActiveStandbyEnum;
import com.huaweicloud.saaslanding.huawei.cdn.enumeration.BusinessTypeEnum;
import com.huaweicloud.saaslanding.huawei.cdn.enumeration.DomainStatusEnum;
import com.huaweicloud.saaslanding.huawei.cdn.enumeration.HttpsStatusEnum;
import com.huaweicloud.saaslanding.huawei.cdn.enumeration.MatchTypeEnum;
import com.huaweicloud.saaslanding.huawei.cdn.enumeration.ObsBucketTypeTypeEnum;
import com.huaweicloud.saaslanding.huawei.cdn.enumeration.OriginTypeEnum;
import com.huaweicloud.saaslanding.huawei.cdn.enumeration.RuleTypeEnum;
import com.huaweicloud.saaslanding.huawei.cdn.enumeration.ServiceAreaEnum;
import com.huaweicloud.saaslanding.huawei.cdn.enumeration.TtlTypeEnum;
import com.huaweicloud.saaslanding.huawei.cdn.model.BackSource;
import com.huaweicloud.saaslanding.huawei.cdn.model.CacheConfigRequest;
import com.huaweicloud.saaslanding.huawei.cdn.model.DomainBody;
import com.huaweicloud.saaslanding.huawei.cdn.model.FlexibleOrigin;
import com.huaweicloud.saaslanding.huawei.cdn.model.FlexibleOriginConfig;
import com.huaweicloud.saaslanding.huawei.cdn.model.HttpInfoRequestBody;
import com.huaweicloud.saaslanding.huawei.cdn.model.Rule;
import com.huaweicloud.saaslanding.huawei.cdn.model.Sources;
import com.huaweicloud.saaslanding.huawei.cdn.request.CdnConfigHttpRequest;
import com.huaweicloud.saaslanding.huawei.cdn.request.CdnCreateCacheConfigRequest;
import com.huaweicloud.saaslanding.huawei.cdn.request.CdnCreateDomainRequest;
import com.huaweicloud.saaslanding.huawei.cdn.request.CdnFlexibleOriginConfigRequest;
import com.huaweicloud.saaslanding.huawei.cdn.response.CdnCreateDomainResponse;
import com.huaweicloud.saaslanding.huawei.cdn.response.CdnDomainResponse;
import com.huaweicloud.saaslanding.huawei.iam.api.IamApi;
import com.huaweicloud.saaslanding.install.applicationservice.CdnService;
import com.huaweicloud.saaslanding.install.properties.Elb;
import com.huaweicloud.saaslanding.install.properties.Endpoint;
import com.huaweicloud.saaslanding.install.properties.Iam;
import com.huaweicloud.saaslanding.install.properties.Ssl;
import com.huaweicloud.saaslanding.install.util.DomainNameUtil;

import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;

import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * CDN加速域名 实现类
 *
 * @since 2023-03-28
 */
@Slf4j
@Service
public class CdnServiceImpl implements CdnService {

    /**
     * 获取CDN加速域名状态，最大请求次数
     */
    private static final Integer MAX_REQUEST_TIME = 40;

    /**
     * 获取CDN加速域名状态，请求间隔
     */
    private static final Integer REQUEST_INTERVAL = 30000;

    /**
     * 禁用CDN加速域名和删除的等待间隔
     */
    private static final Integer DISABLE_DELETE_INTERVAL = 2 * 60 * 1000;

    /**
     * 后端不缓存路径
     */
    private static final String BACKEND_UNCACHED_PATH = "/api";

    /**
     * 访问后端缓存规则设置的时间
     */
    private static final Integer BACKEND_CACHE_TTL = 0;

    /**
     * 访问后端缓存规则设置的优先级
     */
    private static final Integer BACKEND_CACHE_RULE_PRIORITY = 100;

    /**
     * 高级回源匹配路径
     */
    private static final String FLEXIBLE_ORIGIN_MATCH_PATTERN = "/obs";

    /**
     * 高级回源规则设置的优先级
     */
    private static final Integer FLEXIBLE_ORIGIN_PRIORITY = 100;

    @Resource
    private Iam iam;

    @Resource
    private Endpoint endpoint;

    @Resource
    private Elb elb;

    @Resource
    private Ssl ssl;

    @Value("${saaslanding.platformAppName}")
    private String platformAppName;

    @Value("${saaslanding-install.cdn.enabled:false}")
    private Boolean cdnEnabled;

    @Value("${saaslanding-install.waf.enabled:false}")
    private Boolean wafEnabled;

    @Value("${saaslanding-install.domain.certificate}")
    private String certificate;

    @Value("${saaslanding-install.domain.certificate-key}")
    private String certificateKey;

    @Resource
    private WafHostResourceBaseMapper wafHostResourceBaseMapper;

    @Resource
    private CdnResourceBaseMapper cdnResourceBaseMapper;

    @Resource
    private TenantAppDomainBaseMapper tenantAppDomainBaseMapper;

    @Resource
    private AppTemplateBaseMapper appTemplateBaseMapper;

    @Resource
    private ObsResourceBaseMapper obsResourceBaseMapper;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createAndConfigDomain() {

        if (!cdnEnabled) {
            log.info("----------------CDN未开启，创建加速域名跳过---------------");
            return null;
        }

        log.info("----------------start：创建CDN加速域名---------------");
        tenantAppDomainBaseMapper.updateStatus(platformAppName, TenantAppDomainStatusEnum.CDN_CONFIGURING.getStatus());

        String cdnDomainName = generateDomainName();

        Map<String, String> headers = IamApi.getIamHeader(iam.getEndpoint(), iam.getIamDomain(), iam.getIamUser(),
            iam.getIamPassword(), iam.getProjectId());

        CdnCreateDomainRequest cdnCreateDomainRequest = generateCdnCreateDomainRequest(cdnDomainName);

        CdnCreateDomainResponse response = CdnDomainApi.create(endpoint.getCdn(), cdnCreateDomainRequest, headers);

        String domainId = response.getDomain().getId();
        String cname = response.getDomain().getCname();

        CdnResource cdnResource = new CdnResource();
        cdnResource.setDomainId(domainId)
            .setDomainName(cdnDomainName)
            .setIpOrDomain(response.getDomain().getSources().get(0).getIpOrDomain())
            .setBusinessType(BusinessTypeEnum.WEB.getType())
            .setOriginType(response.getDomain().getSources().get(0).getOriginType())
            .setCname(cname)
            .setStatus(response.getDomain().getDomainStatus())
            .setTenantCode(platformAppName);
        cdnResourceBaseMapper.insert(cdnResource);

        log.info("----------------end：创建CDN加速域名---------------");

        updateTenantAppDomain(cdnResource.getId());

        // 设置缓存规则
        setDomainCacheRules(headers, cdnResource, domainId);

        // 配置Https
        setDomainHttps(headers, cdnResource, domainId);

        // 配置高级回源
        setFlexibleOrigin(headers, cdnResource);

        ThreadPoolManager.newInstance().execute(() -> {

            Boolean isSuccessful;

            try {
                isSuccessful = isDomainCreatedSuccessful(cdnDomainName, headers, cdnResource);
            } catch (Exception e) {
                log.error("获取CDN域名状态失败，CDN加速域名创建及配置失败: {}", e.toString());
                throw new BizException(ExceptionCode.INITIALIZE_CDN_FAILED);
            }

            if (Boolean.FALSE.equals(isSuccessful)) {
                tenantAppDomainBaseMapper.updateStatus(platformAppName,
                    TenantAppDomainStatusEnum.CDN_CONFIGURE_FAILED.getStatus());
                log.error("CDN加速域名创建及配置失败");
                throw new BizException(ExceptionCode.INITIALIZE_CDN_FAILED);
            }

            tenantAppDomainBaseMapper.updateStatus(platformAppName,
                TenantAppDomainStatusEnum.CDN_CONFIGURE_SUCCESS.getStatus());
        });

        return cdnResource.getId();
    }

    @Override
    public void deleteDomain() {
        if (!cdnEnabled) {
            log.info("----------------CDN未开启，删除加速域名跳过---------------");
            return;
        }

        log.info("----------------start：删除CDN加速域名---------------");
        List<CdnResource> cdnResources =
            cdnResourceBaseMapper.selectList(new QueryWrap<CdnResource>().lambda().eq(CdnResource::getIsDel, false));

        Map<String, String> headers = IamApi.getIamHeader(iam.getEndpoint(), iam.getIamDomain(), iam.getIamUser(),
            iam.getIamPassword(), iam.getProjectId());

        // 为避免删除CDN加速域名占用过长时间，新起线程执行clean操作
        ThreadPoolManager.newInstance().execute(() -> {
            try {
                disableAndDeleteDomain(cdnResources, headers);
            } catch (InterruptedException e) {
                log.error("停用并删除CDN加速域名，线程被中断：{}", e.toString());
                Thread.currentThread().interrupt();
            }
        });

        log.info("----------------end：删除CDN加速域名---------------");
    }

    /**
     * 生成加速域名
     *
     * @return 加速域名
     */
    private String generateDomainName() {
        String domainName = DomainNameUtil.getDomainName();
        if (StringUtils.isEmpty(domainName)) {
            log.error("ISV域名为空！");
            throw new BizException(ExceptionCode.DOMAIN_IS_NULL);
        }

        return domainName.replaceFirst(domainName.substring(0, domainName.indexOf(".")), "*");
    }

    @NotNull
    private String generateObsDomain(String obsName) {
        return obsName.concat(".").concat(endpoint.getObs());
    }

    /**
     * 生成CDN创建加速域名请求体
     *
     * @param cdnDomainName 加速域名
     * @return CDN创建加速域名请求体
     */
    @NotNull
    private CdnCreateDomainRequest generateCdnCreateDomainRequest(String cdnDomainName) {
        Sources source = new Sources();
        source.setActiveStandby(ActiveStandbyEnum.MAIN_SITE.getStatus());

        if (!wafEnabled) {
            source.setOriginType(OriginTypeEnum.IP.getType()).setIpOrDomain(elb.getElbEip());
        } else {
            List<WafHostResource> wafHostResources = wafHostResourceBaseMapper.getByTenantCode(platformAppName);
            if (wafHostResources == null || wafHostResources.isEmpty()) {
                log.error("WAF防护域名信息为空！");
                throw new BizException(ExceptionCode.WAF_DOMAIN_IS_NULL);
            }

            source.setOriginType(OriginTypeEnum.DOMAIN.getType()).setIpOrDomain(wafHostResources.get(0).getCname());
        }

        List<Sources> sources = new ArrayList<>();
        sources.add(source);

        DomainBody domainBody = new DomainBody();
        domainBody.setDomainName(cdnDomainName)
            .setBusinessType(BusinessTypeEnum.WEB.getType())
            .setServiceArea(ServiceAreaEnum.MAINLAND_CHINA.getArea())
            .setEnterpriseProjectId(iam.getEnterpriseProjectId())
            .setSources(sources);

        CdnCreateDomainRequest cdnCreateDomainRequest = new CdnCreateDomainRequest();
        cdnCreateDomainRequest.setDomain(domainBody);
        return cdnCreateDomainRequest;
    }

    /**
     * 配置CDN加速域名缓存规则
     *
     * @param headers 请求头
     * @param cdnResource CDN加速域名资源信息
     * @param domainId CDN加速域名ID
     */
    private void setDomainCacheRules(Map<String, String> headers, CdnResource cdnResource, String domainId) {
        log.info("----------------start：配置CDN加速域名缓存规则---------------");

        Rule rule = new Rule();
        rule.setRuleType(RuleTypeEnum.DIRECTORY.getType())
            .setContent(BACKEND_UNCACHED_PATH)
            .setTtl(BACKEND_CACHE_TTL)
            .setTtlType(TtlTypeEnum.SECOND.getType())
            .setPriority(BACKEND_CACHE_RULE_PRIORITY);
        List<Rule> rules = new ArrayList<>();
        rules.add(rule);

        CacheConfigRequest cacheConfigRequest = new CacheConfigRequest();
        cacheConfigRequest.setRules(rules);

        CdnCreateCacheConfigRequest cdnCreateCacheConfigRequest = new CdnCreateCacheConfigRequest();
        cdnCreateCacheConfigRequest.setCacheConfig(cacheConfigRequest);

        try {
            CdnCacheApi.set(endpoint.getCdn(), domainId, iam.getEnterpriseProjectId(), cdnCreateCacheConfigRequest,
                headers);
        } catch (Exception e) {
            log.error(e.toString());
            cdnResource.setStatus(DomainStatusEnum.CONFIGURE_FAILED.getStatus());
            cdnResourceBaseMapper.updateById(cdnResource);
            tenantAppDomainBaseMapper.updateStatus(platformAppName,
                TenantAppDomainStatusEnum.CDN_CONFIGURE_FAILED.getStatus());
            throw new BizException(ExceptionCode.INITIALIZE_CDN_FAILED);
        }

        cdnResourceBaseMapper.updateById(cdnResource);

        log.info("----------------end：配置CDN加速域名缓存规则---------------");
    }

    /**
     * 配置CDN加速域名Https
     *
     * @param headers 请求头
     * @param cdnResource CDN加速域名资源信息
     * @param domainId CDN加速域名ID
     */
    private void setDomainHttps(Map<String, String> headers, CdnResource cdnResource, String domainId) {
        if (ssl.isEnabled()) {
            log.info("----------------start：配置CDN加速域名Https---------------");

            HttpInfoRequestBody httpInfoRequestBody = new HttpInfoRequestBody();
            httpInfoRequestBody.setCertName(generateCertificateName())
                .setHttpsStatus(HttpsStatusEnum.ENABLE_HTTPS_ORIGIN_HTTPS.getStatus())
                .setCertificate(certificate)
                .setPrivateKey(certificateKey);

            CdnConfigHttpRequest cdnConfigHttpRequest = new CdnConfigHttpRequest();
            cdnConfigHttpRequest.setHttps(httpInfoRequestBody);

            try {
                CdnHttpsConfigApi.set(endpoint.getCdn(), domainId, iam.getEnterpriseProjectId(), cdnConfigHttpRequest,
                    headers);
            } catch (Exception e) {
                log.error(e.toString());
                cdnResource.setStatus(DomainStatusEnum.CONFIGURE_FAILED.getStatus());
                cdnResourceBaseMapper.updateById(cdnResource);
                tenantAppDomainBaseMapper.updateStatus(platformAppName,
                    TenantAppDomainStatusEnum.CDN_CONFIGURE_FAILED.getStatus());
                throw new BizException(ExceptionCode.INITIALIZE_CDN_FAILED);
            }

            cdnResourceBaseMapper.updateById(cdnResource);

            log.info("----------------end：配置CDN加速域名Https---------------");
        }
    }

    /**
     * 配置CDN高级回源
     *
     * @param headers 请求头
     * @param cdnResource CDN加速域名资源信息
     */
    private void setFlexibleOrigin(Map<String, String> headers, CdnResource cdnResource) {
        ObsResource obsResource = obsResourceBaseMapper.getTenantBucket(platformAppName);
        log.info("----------------start：配置CDN高级回源---------------");

        if (CheckNullUtil.isNull(obsResource)) {
            log.warn("该租户无OBS桶，无法配置CDN高级回源！");
        }

        BackSource backSource = new BackSource();
        backSource.setSourcesType(OriginTypeEnum.OBS_BUCKET.getType())
            .setIpOrDomain(generateObsDomain(obsResource.getObsName()))
            .setObsBucketType(ObsBucketTypeTypeEnum.PRIVATE.getType());

        List<BackSource> backSources = new ArrayList<>();
        backSources.add(backSource);

        FlexibleOrigin flexibleOrigin = new FlexibleOrigin();
        flexibleOrigin.setMatchType(MatchTypeEnum.FILE_PATH.getType())
            .setMatchPattern(FLEXIBLE_ORIGIN_MATCH_PATTERN)
            .setBackSources(backSources)
            .setPriority(FLEXIBLE_ORIGIN_PRIORITY);

        List<FlexibleOrigin> flexibleOriginList = new ArrayList<>();
        flexibleOriginList.add(flexibleOrigin);

        FlexibleOriginConfig flexibleOriginConfig = new FlexibleOriginConfig();
        flexibleOriginConfig.setFlexibleOrigin(flexibleOriginList);

        CdnFlexibleOriginConfigRequest cdnFlexibleOriginConfigRequest = new CdnFlexibleOriginConfigRequest();
        cdnFlexibleOriginConfigRequest.setConfigs(flexibleOriginConfig);

        try {
            CdnFlexibleOriginApi.set(endpoint.getCdn(), cdnResource.getDomainName(), iam.getEnterpriseProjectId(),
                cdnFlexibleOriginConfigRequest, headers);
        } catch (Exception e) {
            log.error(e.toString());
            cdnResource.setStatus(DomainStatusEnum.CONFIGURE_FAILED.getStatus());
            cdnResourceBaseMapper.updateById(cdnResource);
            tenantAppDomainBaseMapper.updateStatus(platformAppName,
                TenantAppDomainStatusEnum.CDN_CONFIGURE_FAILED.getStatus());
            throw new BizException(ExceptionCode.INITIALIZE_CDN_FAILED);
        }

        log.info("----------------end：配置CDN高级回源---------------");
    }

    /**
     * 判断CDN加速域名是否创建成功
     *
     * @param cdnDomainName CDN加速域名
     * @param headers 请求头
     * @param cdnResource CDN加速域名资源信息
     * @return 是否创建成功
     * @throws InterruptedException
     */
    private Boolean isDomainCreatedSuccessful(String cdnDomainName, Map<String, String> headers,
        CdnResource cdnResource) throws InterruptedException {
        for (int i = 0; i < MAX_REQUEST_TIME; i++) {
            Thread.sleep(REQUEST_INTERVAL);

            CdnDomainResponse cdnDomainResponse =
                CdnDomainApi.getDetail(endpoint.getCdn(), cdnDomainName, iam.getEnterpriseProjectId(), headers);

            String status = cdnDomainResponse.getDomain().getDomainStatus();
            cdnResource.setStatus(status);
            cdnResourceBaseMapper.updateById(cdnResource);

            if (DomainStatusEnum.ONLINE.getStatus().equals(status)) {
                log.info("CDN加速域名已上线");
                return true;
            } else if (DomainStatusEnum.CONFIGURE_FAILED.getStatus()
                .equals(cdnDomainResponse.getDomain().getDomainStatus())
                || DomainStatusEnum.CHECK_FAILED.getStatus().equals(status)) {
                log.error("CDN加速域名创建失败，失败原因为：{}", cdnDomainResponse.getDomain().getDomainStatus());
                return false;
            } else if (DomainStatusEnum.OFFLINE.getStatus().equals(status)) {
                log.warn("CDN加速域名已下线");
                return false;
            } else if (DomainStatusEnum.DELETING.getStatus().equals(status)) {
                log.warn("CDN加速域名删除中");
                return false;
            } else {
                log.info("CDN加速域名配置/审核中");
            }
        }
        return false;
    }

    /**
     * 停用并删除CDN加速域名
     *
     * @param cdnResources CDN资源
     * @param headers 请求头
     * @throws InterruptedException
     */
    private void disableAndDeleteDomain(List<CdnResource> cdnResources, Map<String, String> headers)
        throws InterruptedException {
        // 先禁用加速域名，再删除
        for (CdnResource resource : cdnResources) {
            if (StringUtils.isNotBlank(resource.getDomainId())) {
                CdnDomainApi.disable(endpoint.getCdn(), resource.getDomainId(), iam.getProjectId(), headers);
            }
        }

        Thread.sleep(DISABLE_DELETE_INTERVAL);

        for (CdnResource resource : cdnResources) {
            if (StringUtils.isNotBlank(resource.getDomainId())) {
                CdnDomainApi.delete(endpoint.getCdn(), resource.getDomainId(), iam.getProjectId(), headers);
            }
        }
    }

    /**
     * 生成证书名称
     *
     * @return 证书名称
     */
    @NotNull
    private String generateCertificateName() {
        return platformAppName.concat("证书");
    }

    /**
     * 更新租户应用域名
     *
     * @param cdnResourceId CDN加速域名记录ID
     */
    private void updateTenantAppDomain(Long cdnResourceId) {
        AppTemplate saasAppInfo = appTemplateBaseMapper.getSaasAppInfo();
        TenantAppDomain tenantAppDomain =
            tenantAppDomainBaseMapper.getByTenantAndApp(platformAppName, saasAppInfo.getId());
        tenantAppDomain.setCdnResourceId(cdnResourceId)
            .setStatus(TenantAppDomainStatusEnum.CDN_CONFIGURING.getStatus());
        tenantAppDomainBaseMapper.updateById(tenantAppDomain);
    }

}
